package com.atyang.aspect;

import com.atyang.annotation.RedisLock;
import com.atyang.utils.RedisUtil;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

@Aspect
@Component
public class RedisLockAspect {
    private static final Logger logger = LoggerFactory.getLogger(RedisLockAspect.class);

    private static final String LOCK_PREFIX = "lockPrefix";
    private static final String LOCK_KEY = "lockKey";
    private static final String TIME_OUT = "timeOut";
    private static final int PROTECT_TIME = 2 << 11; // 4096
    private static final Integer MAX_RETRY_COUNT = 3;

    @Resource
    private RedisUtil redisUtil;

    /**
     * 切点就是redis注解
     */
    @Pointcut("@annotation(com.atyang.annotation.RedisLock)")
    public void redisLockAspect(){}


    @Around("redisLockAspect()")
    public void lockAroundAction(ProceedingJoinPoint proceeding) {
        // 获取redis锁
        boolean flag = this.getLock(proceeding, 0, System.currentTimeMillis());
        if (flag){
            try {
                proceeding.proceed();
                // 防止集群系统时间不一致导致定时任务重复执行
                Thread.sleep(PROTECT_TIME);
            } catch (Throwable throwable) {
                throw new RuntimeException("分布式锁执行发生异常" + throwable.getMessage(), throwable);
            } finally {
                this.delLock(proceeding);
            }
        }else {
            logger.info("其他系统正在执行此项任务");
        }
    }


    /**
     * 获取锁
     * @param proceeding
     * @param count
     * @param currentTime
     * @return
     */
    private boolean getLock(ProceedingJoinPoint proceeding, int count, long currentTime){
        // 获取注解中的参数
        Map<String, Object> annotationArgs = this.getAnnotationArgs(proceeding);
        String lockPrefix =(String) annotationArgs.get(LOCK_PREFIX);
        String lockKey = (String) annotationArgs.get(LOCK_KEY);
        long expire = (Long) annotationArgs.get(TIME_OUT);
        if (StringUtils.isEmpty(lockPrefix) || StringUtils.isEmpty(lockKey)){
            logger.error("RedisLock 锁前缀, 锁名未设置");
        }
        if (redisUtil.setNx(lockPrefix,lockKey,expire)){
            return true;
        }else {
            // 写锁的时间与当前时间差 > 过期时间加上保护时间 则强制删除锁，防止死锁
            long createTime = redisUtil.getLongValue(lockPrefix, lockKey);
            if ((currentTime - createTime) > (expire * 1000 + PROTECT_TIME)){
                count ++;
                if (count > MAX_RETRY_COUNT){
                    return false;
                }
                redisUtil.delete(lockPrefix,lockKey);
                getLock(proceeding,count,currentTime);
            }
            return false;
        }
    }

    /**
     * 获取注解参数
     * @param proceeding
     * @return
     */
    private Map<String, Object> getAnnotationArgs(ProceedingJoinPoint proceeding){
        Class<?> target = proceeding.getTarget().getClass();
        Method[] methods = target.getMethods();
        String methodName = proceeding.getSignature().getName();
        for (Method method : methods) {
            if (method.getName().equals(methodName)){
                Map<String, Object> result = new HashMap<String, Object>();
                RedisLock redisLock = method.getAnnotation(RedisLock.class);
                result.put(LOCK_PREFIX, redisLock.lockPrefix());
                result.put(LOCK_KEY, redisLock.lockKey());
                result.put(TIME_OUT, redisLock.timeUnit().toSeconds(redisLock.timeOut()));
                return result;
            }
        }
        return null;
    }

    /**
     * 删除锁
     * @param proceeding
     */
    private void delLock(ProceedingJoinPoint proceeding){
        Map<String, Object> annotationArgs = this.getAnnotationArgs(proceeding);
        String prefix = (String) annotationArgs.get(LOCK_PREFIX);
        String key = (String) annotationArgs.get(LOCK_KEY);
        redisUtil.delete(prefix,key);
    }

}
